cron風タスクスケジューリングをpure Javaで実装してElastic Beanstalkにデプロイする

cron風タスクスケジューリングをpure Javaで実装してElastic Beanstalkにデプロイする

Clock Icon2013.02.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。

一般的に、AWS上にアプリケーションを構築する際、そのメインとなるのはWebアプリであることが多いと思います。ご存知の通り、Webアプリは、ユーザからのHTTPリクエストに応じてHTTPレスポンスを返すアプリケーションです。

しかし、少々凝ったシステムを作る場合、Webアプリに加えて、タスクスケジューリングを利用したバッチアプリが必要になる場合があります。例えば、定期的にメールを送信したい、定期的にTwitterでつぶやいたりタイムラインをチェックを実施したい、等です。

このようなアプリケーションは、HTTPを介した処理ではないので、Webアプリではありません。今回は、このようなスケジューリングを利用したバッチアプリケーション(以下、scheduled-batch)について考えていきたいと思います。

スケジューリングに関する考察色々

まずは、このようなアプリに利用するタスクスケジューラについて、色々考えてみましょう。

クラウドにおけるスケジューリング機能

こういったscheduled-batchの機能は、メインとなるWebアプリと同じドメイン(問題領域)で動作するが故に、従来、Webアプリと中に一緒くたにされて、同じパッケージの中に組み込まれてしまうことが良くあったと思います。つまり、Javaにおいてはwarをデプロイすると、HTTPリクエストのハンドリング機能と共に、スケジューラ機能も動き出すような構成です。

しかし、規模に応じてスケールするWebアプリを構築する場合、Webアプリがscheduled-batchの機能を内包してしまうと、問題が発生します。例えば、スケールアウトしてアプリケーションサーバが2台体制になったらどうなるでしょうか。ユーザへの定期メールは2通届いてしまうでしょう。

従って、スケールアウトを前提としたシステムでは、scheduled-batchの機能は、Webアプリとは別のアプリとして構築する必要があります。

一般的なタスクスケジューラ

一般的に、定期タスクの実行はLinuxにおいてはcron、WindowsではTask Schedulerの役目ですね。このように、プラットフォーム(OS)に依存した機能を利用して実現する事が多いようです。(そこまで拘るべき事柄でもない場合が多いですが)Javaは、Write once, run anywhere、つまりプラットフォームに依存しない実装を行う事ができる言語として定評があります。従って、scheduled-batchをLinuxでもWindowsでも、同じコード及び設定で動かしたい、と思うこともあると思います。

また別の視点から、cron等で叩く対象はシェルスクリプトであることが多々ありますが、WebアプリケーションをJavaで組んだのであれば、scheduled-batchもJavaで組みたいですよね。ドメインモデルが共有できますし。というか、細かい事は抜きにして、Javaネイティブな開発者ならばscheduled-batchもJavaで組みたいわけですよ *1

しかし、cronで叩いて実行するアプリケーションをJavaで組んだ場合、今度は毎回毎回、JVMをはじめとする環境(DIコンテナ等)の立ち上げが必要になります。起動だけで一定時間を消費してしまう可能性もあるのです。短い間隔で実行するタスクの場合や、実行のタイミングを厳密にしたい場合、これが問題になります。

結果として、scheduled-batchは、cronベースではなく、Javaのアプリケーションとして構築することが適切な場合があります。ただし、全てがそう、という訳ではなく。色々なケースがあると思いますが。

具体的なイメージとしては、javaコマンドによってscheduled-batchを起動すると、CTRL+C等で終了するまで、設定に従って適切なタイミングで適切な処理をコールし続けるようなアプリケーションです。

AWSでのscheduled-batchを考えてみる

AWSにおいては、全てのEC2インスタンスは「いつかは落ちる」という前提でインフラを構築して行きます。Webアプリの場合、マルチAZ構成の冗長化によりavailabilityを確保します。さらに、Auto Scalingによりavailabilityを強化しつつ、scalabilityを確保します。

ただ、scheduled-batchシステムで高いavailabilityやscalabilityを確保するには、アーキテクチャレベルから様々な工夫が必要です。そこで、今回はひとまずscalabilityは考えないことにします。つまり、1回1回のジョブの計算量が多くなってきても、並列化によるパフォーマンスの向上が用意に実現できなくても良い、ということです。また、availabilityもWebアプリ水準のものを求めないことにします。つまり、何らかの障害が発生した場合、即座に復旧して途切れのないスケジューリングを続ける、といったことは要求しないことにします。ただし、障害が発生した場合は、多少のダウンタイムが発生するものの、自動復旧ができるような体制を考えたいと思います。

Elastic Beanstalkをscheduled-batchの基盤に利用する

そこで考えたのが、scheduled-batchのホスティングにElastic Beanstalkを使う、というアイデアです。Elastic Beanstalkは、本来WebアプリをホストするためのPaaSサービスです。従って、バッチアプリケーションに使うというのは少々奇抜なアイデアかもしれません。しかし、ヘルスチェックによってシステムのダウンを検知し、新しいインスタンスを起動してくれる、というAuto Scalingの機能は上手く利用できそうです。さらに、アプリケーションのログを収集してS3にアップロードしてくれる機能等も、とても便利ですね。(単独のJava CLIアプリケーションで実装してしまうと、ログの収集についても何らかの手を打たなければならなくなります。)

但し、Auto Scalingのスケールアップ/ダウン動作は無効(Maxを1にする)にしておく必要があります。さもないと、前述のような、メールが複数届いてしまうような事態が発生してしまいます。

というわけで、HTTPリクエストを処理するコードはほとんどありませんが、粛々とスケジューラを回すwarコンポーネントを作ってみました。そしてこれをElastic Beanstalkにデプロイする、というアイデアを実行してみたいと思います。

サンプルアプリケーション解説

まずはgithubからサンプルアプリケーションをcloneしてください。このscheduled-batchは、とりあえず意味もなくpetrucciと名付けました。

$ git clone https://github.com/miyamoto-daisuke/petrucci.git

前提動作環境

まず、petrucciはJava 6で記述されています。Java6がEOLを迎えているのは重々承知ですが…。現在、素の状態のElastic Beanstalkはopenjdk-6で動いているためです。

ビルド及び実行にはMavenを利用しています。Maven3系であれば問題なく動くはずです。2系でも多分動くと思います。

また、AWSのAPIを利用しますので、AWSのアクセスキー及びシークレットが必要になります。 ~/.m2/settings.xml に下記の記述を追加しておいてください。

<settings ...>
  <servers>
    <!-- 追記ここから -->
    <server>
      <id>aws.amazon.com</id>
      <username>[アクセスキー]</username>
      <password>[シークレット]</password>
    </server>
    <!-- 追記ここまで -->
  </servers>
</settings>

さらに、petrucciはAmazon SESを利用してメール送信を行うため、送受信に利用するメールアドレスをverifyしておいてください。

ちなみに本アプリケーションの設定は、us-east-1リージョンで動かすことを前提にしています。他のリージョンで動かす場合は、いくつか設定の変更が必要になると思いますので注意してください。

petrucciの概要

petrucciは以下の2つのジョブを定期実行します。メール送信のジョブがある都合上、petrucciはビルド時にメールの送信先(そして、Fromとしても利用します)の指定が必要です。

  • (A) 毎秒、1行ログ出力を行う。スケジューラが上手く動いていることが、ログから一目瞭然になります *2
  • (B) 2分に一度、us-east-1リージョンのS3バケット一覧と、EC2インスタンス一覧を、指定したメールアドレスに送付します。

といった感じですが、要はpetrucciはscheduled-batchを作成する場合のひな形となることを意図しています。

まずはローカルで動かしてみる

コマンド一発です。

$ mvn jetty:run -Dpetrucci.mailaddress=[From/Toとして利用するメールアドレス]
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building petrucci 1.0.0
[INFO] ------------------------------------------------------------------------
(略)
[INFO] Started Jetty Server
2013/02/25 23:34:55.003 [scheduler-1] INFO jp.classmethod.aws.petrucci.Tasks:73 - 2013/02/25 23:34:55
2013/02/25 23:34:56.001 [scheduler-2] INFO jp.classmethod.aws.petrucci.Tasks:73 - 2013/02/25 23:34:56
2013/02/25 23:34:57.000 [scheduler-3] INFO jp.classmethod.aws.petrucci.Tasks:73 - 2013/02/25 23:34:57
2013/02/25 23:34:58.001 [scheduler-2] INFO jp.classmethod.aws.petrucci.Tasks:73 - 2013/02/25 23:34:58
...

といった感じで、毎秒(A)のタスクが動いていれば正常に起動できています。このまま1〜2分待つと、2分に一度(偶数分の0秒)のタイミングでメールを送信します。指定したメールアドレスにメールが届くことを確認してください。

終了する場合は CTRL+C を入力します。

Elastic Beanstalkで動かしてみる

まず、Elastic Beanstalkが利用するAmazon S3のバケットを作ります。US Standardリージョンに適当な名前のバケットを作成してください。バケット名は、世界中で一意である必要があるので、ここでは特に指示しません。

また、Elastic Beanstalkがアプリケーションを http://[サブドメイン名].elasticbeanstalk.com にデプロイします。この一意なサブドメイン名も適当に決めておきましょう。

その上で、以下のコマンドでpetrucciをデプロイします。このコマンドの実行完了にはしばらく時間が掛かります。筆者が試した場合では、8分弱掛かりました。

$ mvn deploy \
    -Dbeanstalk.s3Bucket=[作成したS3バケット名] \
    -Dbeanstalk.cnamePrefix=[サブドメイン名] \
    -Dpetrucci.mailaddress=[From/Toとして利用するメールアドレス]
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building petrucci 1.0.0
[INFO] ------------------------------------------------------------------------
(略)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7:49.670s
[INFO] Finished at: Xxx Xxx XX XX:XX:XX XXX XXXX
[INFO] Final Memory: 21M/204M
[INFO] ------------------------------------------------------------------------

デプロイが成功すると、やはり2分に1度のペースでメールが届くはずです。メールボックスを確認してください。

さらに、興味のある方は、EC2のManagement Consoleから、インスタンスを強制Terminateしてみてください。しばらく経った後、ヘルスチェックが失敗し、新しいインスタンスが自動的に立ち上がる様子が観察できると思います。また、メールは数回分スキップされてしまいますが、配信も自動的に再開します。

Elastic Beanstalkへのデプロイの終了

デプロイの終了は以下の通りです。最初のコマンドを実行してから数分〜数十分後に、2つ目のコマンドを入力してください。(2つ目のコマンドを入力しなくても、ひとまず稼働と課金は止まります)

$ mvn beanstalk:terminate-environment -Dbeanstalk.cnamePrefix=[起動時に指定したサブドメイン名]
(environmentが完全にTerminateするまで、数分〜数十分待つ)
$ mvn beanstalk:delete-application

若しくは、Elastic BeanstalkのManagement Consoleから「Delete This Application」を選ぶ方が早いかもしれません。

2013-02-26_1642

コードの概要解説

簡単にpetrucciのコードの解説をしておきます。petrucciはSpring Frameworkタスクスケジューリング機能を使っています。

Springのbean(コンポーネント)設定はpetrucci/src/main/resources/applicationContext.xmlに記述してあります。

(略)
  <context:component-scan base-package="jp.classmethod.aws.petrucci" />
  <context:property-placeholder location="classpath:/aws.properties"/>
  <context:annotation-config />

  <!-- Scheduler -->
  <task:annotation-driven executor="executor" scheduler="scheduler"/>
  <task:executor id="executor" pool-size="5"/>
  <task:scheduler id="scheduler" pool-size="5" />
(略)

実際のタスク記述はTasksクラスに実装してあります。

@Component
public class Tasks implements InitializingBean {

  // 略

  @Scheduled(cron = "* * * * * *")
  public void timeKeeper() {
    DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    logger.info(df.format(new Date()));
  }

  @Scheduled(cron = "0 */2 * * * *")
  public void reportS3AndEC2() {
    logger.info("report start");
    // 略
    logger.info("report end");
  }

  @Override
  public void afterPropertiesSet() throws Exception {
    // 略
  }
}

Spring bean(spring managed instance)のメソッドに@Scheduledアノテーションでcron表記を記述しておくと、そのメソッドがそのスケジュールで定期的に呼び出されるようになります。cronでは分単位までしか指定できませんが、このタスクスケジューリング機能では秒単位の指定も可能のため、6フィールドで構成されています。

もし興味があれば、このクラスにいくつかメソッドを追加し、@Scheduledアノテーションを付けてみてください。

ちなみに、afterPropertiesSetでは、指定したメールアドレスがAmazon SESでverify済みであるかどうかをチェックしています。実際にpetrucciを改変してscheduled-batchシステムを構築する場合は、恐らくこの処理は不要でしょう。その場合、InitializingBeanインターフェイスを実装する必要もありません。

まとめ

というわけで、今回はpure Javaでプラットフォームに依存しないcron風タスクスケジュールアプリのひな形petrucciをご紹介しました。どんなケースでも使えるものではありませんが、選択肢の一つとして道具箱に入れておくと便利なアーキテクチャだと思います。ご活用ください。

そうそう、petrucciの利用許諾はApache License v2.0とします。改変・再配布・フォーク等、ご自由にどうぞ。

脚注

  1. これが本音かw
  2. ログを見て、何と無く不安にならないためのジョブです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.